'use strict';

/* Universial Module (UMD) design pattern
 * https://github.com/umdjs/umd/blob/master/templates/returnExports.js
 */
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // register as an AMD anonymous module
        define([], factory);
    } else if (typeof module === 'object' && module.exports) {
        // use a node.js style export
        module.exports = factory();
    } else {
        // if this isn't running under Node or AMD, just set a global variable
        root.WebAuthn = factory();
    }
    // the return value of this function is what becomes the AMD / CommonJS / Global export
}(this, function () { // eslint-disable-line no-invalid-this

    /*
     * Base64URL-ArrayBuffer
     * https://github.com/herrjemand/Base64URL-ArrayBuffer
     *
     * Copyright (c) 2017 Yuriy Ackermann <ackermann.yuriy@gmail.com>
     * Copyright (c) 2012 Niklas von Hertzen
     * Licensed under the MIT license.
     */
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
    // Use a lookup table to find the index.
    const lookup = new Uint8Array(256);

    for (let i = 0; i < chars.length; i++) {
        lookup[chars.charCodeAt(i)] = i;
    }

    const bufferToBase64 = function (arraybuffer) {
        const bytes = new Uint8Array(arraybuffer);

        let i;
        let len = bytes.length;
        let base64url = '';

        for (i = 0; i < len; i += 3) {
            base64url += chars[bytes[i] >> 2];
            base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
            base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
            base64url += chars[bytes[i + 2] & 63];
        }

        if ((len % 3) === 2) {
            base64url = base64url.substring(0, base64url.length - 1);
        } else if (len % 3 === 1) {
            base64url = base64url.substring(0, base64url.length - 2);
        }

        return base64url;
    }

    const base64ToBuffer = function (base64string) {
        if (base64string) {

            let bufferLength = base64string.length * 0.75;

            let len = base64string.length;
            let i;
            let p = 0;

            let encoded1;
            let encoded2;
            let encoded3;
            let encoded4;

            let bytes = new Uint8Array(bufferLength);

            for (i = 0; i < len; i += 4) {
                encoded1 = lookup[base64string.charCodeAt(i)];
                encoded2 = lookup[base64string.charCodeAt(i + 1)];
                encoded3 = lookup[base64string.charCodeAt(i + 2)];
                encoded4 = lookup[base64string.charCodeAt(i + 3)];

                bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
                bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
                bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
            }

            return bytes.buffer;
        }
    }

    /*
     * WebAuthn
     * https://github.com/vert-x3/vertx-auth/blob/master/vertx-auth-webauthn/src/main/js/vertx-auth-webauthn.js
     *
     * Copyright (c) 2020 Paulo Lopes <pmlopes@gmail.com>
     * Licensed under the Apache 2 license.
     */

    function WebAuthn(options) {
        this.registerPath = options.registerPath;
        this.loginPath = options.loginPath;
        this.callbackPath = options.callbackPath;
        // validation
        if (!this.callbackPath) {
            throw new Error('Callback path is missing!');
        }
    }

    WebAuthn.constructor = WebAuthn;

    WebAuthn.prototype.register = function (user) {
        const self = this;
        if (!self.registerPath) {
            return Promise.reject('Register path missing form the initial configuration!');
        }
        return fetch(self.registerPath, {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(user || {})
        })
            .then(res => {
                if (res.status === 200) {
                    return res;
                }
                throw new Error(res.statusText);
            })
            .then(res => res.json())
            .then(res => {
                res.challenge = base64ToBuffer(res.challenge);
                res.user.id = base64ToBuffer(res.user.id);
                if (res.excludeCredentials) {
                    for (let i = 0; i < res.excludeCredentials.length; i++) {
                        res.excludeCredentials[i].id = base64ToBuffer(res.excludeCredentials[i].id);
                    }
                }
                return res;
            })
            .then(res => navigator.credentials.create({publicKey: res}))
            .then(credential => {
                return fetch(self.callbackPath, {
                    method: 'POST',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        id: credential.id,
                        rawId: bufferToBase64(credential.rawId),
                        response: {
                            attestationObject: bufferToBase64(credential.response.attestationObject),
                            clientDataJSON: bufferToBase64(credential.response.clientDataJSON)
                        },
                        type: credential.type
                    }),
                })
            })
            .then(res => {
                if (res.status >= 200 && res.status < 300) {
                    return res;
                }
                throw new Error(res.statusText);
            });
    };

    WebAuthn.prototype.login = function (user) {
        const self = this;
        if (!self.loginPath) {
            return Promise.reject('Login path missing from the initial configuration!');
        }
        return fetch(self.loginPath, {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(user)
        })
            .then(res => {
                if (res.status === 200) {
                    return res;
                }
                throw new Error(res.statusText);
            })
            .then(res => res.json())
            .then(res => {
                res.challenge = base64ToBuffer(res.challenge);
                if (res.allowCredentials) {
                    for (let i = 0; i < res.allowCredentials.length; i++) {
                        res.allowCredentials[i].id = base64ToBuffer(res.allowCredentials[i].id);
                    }
                }
                return res;
            })
            .then(res => navigator.credentials.get({publicKey: res}))
            .then(credential => {
                return fetch(self.callbackPath, {
                    method: 'POST',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        id: credential.id,
                        rawId: bufferToBase64(credential.rawId),
                        response: {
                            clientDataJSON: bufferToBase64(credential.response.clientDataJSON),
                            authenticatorData: bufferToBase64(credential.response.authenticatorData),
                            signature: bufferToBase64(credential.response.signature),
                            userHandle: bufferToBase64(credential.response.userHandle),
                        },
                        type: credential.type
                    }),
                })
            })
            .then(res => {
                if (res.status >= 200 && res.status < 300) {
                    return res;
                }
                throw new Error(res.statusText);
            });
    };

    return WebAuthn;
}));
